// Copyright (c) Appeon
// The following has been modified by Appeon under compliance with the Apache 2.0 license from the original work

// Copyright(c) 2002-2019, Npgsql
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written agreement
// is hereby granted, provided that the above copyright notice and this
// paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL NPGSQL BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// Npgsql HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// NPGSQL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE.THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND Npgsql
// HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS,
// OR MODIFICATIONS.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.Json;
using NpgsqlTypes;
using SnapObjects.Commons;

namespace SnapObjects.Data.PostgreSql
{
    public class PostgreSqlTypeMapper : TypeMapper
    {
        //String
        private readonly StringTypeMapping _text = new StringTypeMapping("text", DbType.String);
        private readonly StringTypeMapping _varchar = new StringTypeMapping("character varying", DbType.String);
        private readonly PostgreSqlCharTypeMapping _char = new PostgreSqlCharTypeMapping("character");
        private readonly CharTypeMapping _singleChar = new CharTypeMapping("character(1)", DbType.String);
        private readonly StringTypeMapping _stringAsSingleChar = new StringTypeMapping("character(1)");
        private readonly PostgreSqlStringTypeMapping _xml = new PostgreSqlStringTypeMapping("xml", NpgsqlDbType.Xml);
        private readonly PostgreSqlStringTypeMapping _ciText = new PostgreSqlStringTypeMapping("citext", NpgsqlDbType.Citext);
        private readonly PostgreSqlStringTypeMapping _abstime = new PostgreSqlStringTypeMapping("abstime", NpgsqlDbType.Abstime);
        private readonly PostgreSqlStringTypeMapping _unknowString = new PostgreSqlStringTypeMapping("unknownstring", NpgsqlDbType.Unknown);
        private readonly PostgreSqlIntenalStringTypeMapping _internaleString = new PostgreSqlIntenalStringTypeMapping();
        
        //Json
        private readonly PostgreSqlJsonTypeMapping _json
            = new PostgreSqlJsonTypeMapping("json", NpgsqlDbType.Json, typeof(string));

        private readonly PostgreSqlJsonTypeMapping _jsonb
            = new PostgreSqlJsonTypeMapping("jsonb", NpgsqlDbType.Jsonb, typeof(string));

        private readonly PostgreSqlJsonTypeMapping _jsonbDocument
            = new PostgreSqlJsonTypeMapping("jsonb", NpgsqlDbType.Jsonb, typeof(JsonDocument));

        private readonly PostgreSqlJsonTypeMapping _jsonbElement
            = new PostgreSqlJsonTypeMapping("jsonb", NpgsqlDbType.Jsonb, typeof(JsonElement));

        //Binary
        private readonly PostgreSqlByteArrayTypeMapping _bytea
            = new PostgreSqlByteArrayTypeMapping("bytea", DbType.Binary);

        //Number
        private readonly PostgreSqlDoubleTypeMapping _double = new PostgreSqlDoubleTypeMapping("float"); // Note: "float" is correct SQL Server type to map to CLR-type double
        private readonly PostgreSqlFloatTypeMapping _real = new PostgreSqlFloatTypeMapping("real"); // Note: "real" is correct SQL Server type to map to CLR-type float
        private readonly PostgreSqlIntTypeMapping _int = new PostgreSqlIntTypeMapping("integer", DbType.Int32);
        private readonly LongTypeMapping _long = new LongTypeMapping("bigint", DbType.Int64);
        private readonly PostgreSqlShortTypeMapping _short = new PostgreSqlShortTypeMapping("smallint", DbType.Int16);
        private readonly PostgreSqlByteTypeMapping _byte = new PostgreSqlByteTypeMapping("tinyint", DbType.Byte);
        private readonly DecimalTypeMapping _decimal = new DecimalTypeMapping("decimal(18, 2)", DbType.Decimal);
        private readonly PostgreSqlDoubleTypeMapping _doubleprecision = new PostgreSqlDoubleTypeMapping("double precision");

        private readonly PostgreSqlUIntTypeMapping _xid = new PostgreSqlUIntTypeMapping("xid", NpgsqlDbType.Xid);
        private readonly PostgreSqlUIntTypeMapping _oid = new PostgreSqlUIntTypeMapping("oid", NpgsqlDbType.Oid);
        private readonly PostgreSqlUIntTypeMapping _cid = new PostgreSqlUIntTypeMapping("cid", NpgsqlDbType.Cid);
        private readonly PostgreSqlUIntTypeMapping _regtype = new PostgreSqlUIntTypeMapping("regtype", NpgsqlDbType.Regtype);
        private readonly PostgreSqlUIntTypeMapping _lo = new PostgreSqlUIntTypeMapping("lo", NpgsqlDbType.Oid);

        //DateTime/Time
        private readonly PostgreSqlDateTimeTypeMapping _date
            = new PostgreSqlDateTimeTypeMapping("date", NpgsqlDbType.Date);

        private readonly PostgreSqlDateTimeTypeMapping _timeStampWithTimezone
            = new PostgreSqlDateTimeTypeMapping("timestamp with time zone", dbType: NpgsqlDbType.TimestampTz);

        private readonly PostgreSqlDateTimeTypeMapping _timeStampWithoutTimezone
            = new PostgreSqlDateTimeTypeMapping("timestamp without time zone", dbType: NpgsqlDbType.Timestamp);

        private readonly PostgreSqlDateTimeOffsetTypeMapping _dateTimeoffset = new PostgreSqlDateTimeOffsetTypeMapping("timestamp with time zone");

        private readonly PostgreSqlTimeTypeMapping _time
            = new PostgreSqlTimeTypeMapping("time without time zone");

        private readonly PostgreSqlTimeTzTypeMapping _timeTz
            = new PostgreSqlTimeTzTypeMapping("time with time zone");

        private readonly PostgreSqlIntervalTypeMapping _interval
            = new PostgreSqlIntervalTypeMapping();

        //UUID
        private readonly GuidTypeMapping _uuid = new GuidTypeMapping("uuid", DbType.Guid);

        //Bit
        private readonly PostgreSqlVarbitTypeMapping _varbit = new PostgreSqlVarbitTypeMapping("varbit", typeof(BitArray));
        private readonly PostgreSqlBitTypeMapping _bit = new PostgreSqlBitTypeMapping();

        //NpgsqlTypes
        private readonly PostgreSqlCidrTypeMapping _cidr = new PostgreSqlCidrTypeMapping();
        private readonly PostgreSqlMacaddrTypeMapping _macaddr = new PostgreSqlMacaddrTypeMapping();
        private readonly PostgreSqlMacaddr8TypeMapping _macaddr8 = new PostgreSqlMacaddr8TypeMapping();
        private readonly PostgreSqlInetTypeMapping _inet = new PostgreSqlInetTypeMapping();
        private readonly PostgreSqlTidTypeMapping _tid = new PostgreSqlTidTypeMapping();

        private readonly PostgreSqlPointTypeMapping _point = new PostgreSqlPointTypeMapping();
        private readonly PostgreSqlLineTypeMapping _line = new PostgreSqlLineTypeMapping();
        private readonly PostgreSqlLineSegmentTypeMapping _lseg = new PostgreSqlLineSegmentTypeMapping();
        private readonly PostgreSqlPathTypeMapping _path = new PostgreSqlPathTypeMapping();
        private readonly PostgreSqlPolygonTypeMapping _polygon = new PostgreSqlPolygonTypeMapping();
        private readonly PostgreSqlCircleTypeMapping _circle = new PostgreSqlCircleTypeMapping();
        private readonly PostgreSqlBoxTypeMapping _box = new PostgreSqlBoxTypeMapping();
        private readonly PostgreSqlTsQueryTypeMapping _tsquery = new PostgreSqlTsQueryTypeMapping();

        private readonly PostgreSqlReconfigTypeMapping _reconfig = new PostgreSqlReconfigTypeMapping();

        private readonly PostgreSqlHstoreTypeMapping _hstore
            = new PostgreSqlHstoreTypeMapping("hstore", typeof(IDictionary<string, string>), null, true);

        private readonly PostgreSqlTsVectorTypeMapping _tsvector = new PostgreSqlTsVectorTypeMapping("tsvector");

        private readonly PostgreSqlInt2VectorTypeMapping _int2Vector = new PostgreSqlInt2VectorTypeMapping();
        private readonly PostgreSqlOidVectorTypeMapping _oidVector = new PostgreSqlOidVectorTypeMapping();
        private readonly PostgreSqlPglsnTypeMapping _pglsn = new PostgreSqlPglsnTypeMapping();

        //Range
        private readonly PostgreSqlRangeTypeMapping _int4range;
        private readonly PostgreSqlRangeTypeMapping _int8range;
        private readonly PostgreSqlRangeTypeMapping _numrange;
        private readonly PostgreSqlRangeTypeMapping _tsrange;
        private readonly PostgreSqlRangeTypeMapping _tstzrange;
        private readonly PostgreSqlRangeTypeMapping _daterange;

        //Other
        private readonly PostgreSqlBoolTypeMapping _bool = new PostgreSqlBoolTypeMapping("boolean", typeof(bool));

        private readonly PostgreSqlObjectTypeMapping _object
            = new PostgreSqlObjectTypeMapping("object", typeof(object), dbType: DbType.Object);

        private readonly Dictionary<string, TypeMapping> _dbTypeMappings;

        private readonly Dictionary<Type, TypeMapping> _clrTypeMappings;

        public PostgreSqlTypeMapper(IDataContextOptions options) : base(options)
        {
            _int4range = new PostgreSqlRangeTypeMapping("int4range", typeof(NpgsqlRange<int>), _int);
            _int8range = new PostgreSqlRangeTypeMapping("int8range", typeof(NpgsqlRange<long>), _long);
            _numrange = new PostgreSqlRangeTypeMapping("numrange", typeof(NpgsqlRange<decimal>), _decimal);

            _tsrange = new PostgreSqlRangeTypeMapping(
                "tsrange", typeof(NpgsqlRange<DateTime>), _timeStampWithoutTimezone);

            _tstzrange = new PostgreSqlRangeTypeMapping(
                "tstzrange", typeof(NpgsqlRange<DateTime>), _timeStampWithTimezone);

            _daterange = new PostgreSqlRangeTypeMapping(
                "daterange", typeof(NpgsqlRange<DateTime>), _date);

            _dbTypeMappings = new Dictionary<string, TypeMapping>(StringComparer.OrdinalIgnoreCase)
            {
                //string
                { "char", _char },
                { "char(1)", _singleChar },
                { "character varying", _varchar },
                { "character", _char },
                { "character(1)", _singleChar },
                { "varchar", _varchar },
                { "text", _text },
                { "citext", _ciText },
                { "xml", _xml },
                { "name", _varchar},
                { "unknowstring", _unknowString},

                //number
                { "bigint", _long },
                { "dec", _decimal },
                { "decimal", _decimal },
                { "double precision", _doubleprecision },
                { "float", _double },
                { "integer", _int },
                { "numeric", _decimal },
                { "money", _decimal },
                { "tinyint", _byte },
                { "real", _real },
                { "smallint", _short },
                { "smallmoney", _decimal },

                //datetime
                { "abstime",  _abstime},
                { "date", _date },
                { "timestamptz", _timeStampWithTimezone },
                { "timestamp with time zone", _timeStampWithTimezone },
                { "timestamp", _timeStampWithoutTimezone },
                { "timestamp without time zone", _timeStampWithoutTimezone },
                { "datetimeoffset", _dateTimeoffset },
                { "time", _time },
                { "time without time zone", _time },
                { "time with time zone", _timeTz},
                { "interval", _interval},

                //binary
                { "bytea", _bytea},

                //bit
                { "bit", _bit},
                { "varbit", _varbit},
                { "bit varying", _varbit},
                    
                //json
                { "json", _json },
                { "jsonb", _jsonb },

                //npgsql
                { "regconfig", _reconfig },
                { "uuid", _uuid },
                { "boolean", _bool },

                { "inet", _inet},
                { "macaddr", _macaddr},
                { "macaddr8", _macaddr8},
                { "cidr", _cidr},

                { "point", _point},
                { "box", _box},
                { "line", _line},
                { "lseg", _lseg},
                { "polygon", _polygon},
                { "circle", _circle},
                { "path", _path},

                { "xid", _xid},
                { "cid", _cid},
                { "lo", _lo},
                { "oid", _oid},
                { "tid", _tid},
                { "regtype", _regtype},
                { "hstore", _hstore},
                { "tsvector", _tsvector},
                { "tsquery", _tsquery},
                { "int2vector", _int2Vector},
                { "oidvector", _oidVector},
                { "pg_lsn", _pglsn},

                //Range
                { "int4range", _int4range},
                { "int8range", _int8range},
                { "numrange", _numrange},
                { "tsrange", _tsrange},
                { "tstzrange", _tstzrange},
                { "daterange", _daterange}
            };

            // Note: sbyte, ushort, uint, char and ulong type mappings are not supported by SQL Server.
            // We would need the type conversions feature to allow this to work - see https://github.com/aspnet/EntityFramework/issues/242.
            _clrTypeMappings = new Dictionary<Type, TypeMapping>
            {
                { typeof(char), _singleChar },
                { typeof(string), _varchar},
                { typeof(byte), _byte },
                { typeof(int), _int },
                { typeof(long), _long },
                { typeof(double), _double },
                { typeof(short), _short },
                { typeof(float), _real },
                { typeof(decimal), _decimal },
                { typeof(ulong), new ULongTypeMapping("ulong")},
                { typeof(uint), new UIntTypeMapping("uint")},
                { typeof(ushort), new UShortTypeMapping("ushort")},
                { typeof(byte[]), _bytea },
                { typeof(Guid), _uuid },
                { typeof(bool), _bool },
                { typeof(short[]), _oidVector },
                { typeof(TimeSpan), _time },
                { typeof(DateTimeOffset), _dateTimeoffset },
                { typeof(DateTime), _timeStampWithoutTimezone },
                { typeof(Enum), _int},
                { typeof(IDictionary<string,string>), _hstore },
                { typeof(object), _object},
                { typeof(BitArray), _varbit },

                { typeof(JsonDocument), _jsonbDocument },
                { typeof(JsonElement), _jsonbElement },
                { typeof(PhysicalAddress), _macaddr},
                { typeof(IPAddress), _inet },
                { typeof((IPAddress, int)), _cidr },
                { typeof(NpgsqlTid), _tid },
                { typeof(NpgsqlPoint), _point },
                { typeof(NpgsqlBox), _box },
                { typeof(NpgsqlLine), _line },
                { typeof(NpgsqlLSeg), _lseg },
                { typeof(NpgsqlPath), _path },
                { typeof(NpgsqlPolygon), _polygon },
                { typeof(NpgsqlCircle), _circle },
                { typeof(NpgsqlRange<int>), _int4range },
                { typeof(NpgsqlRange<long>), _int8range },
                { typeof(NpgsqlRange<decimal>), _numrange },
                { typeof(NpgsqlRange<DateTime>), _tsrange },
                { typeof(NpgsqlTsQuery), _tsquery },
                { typeof(NpgsqlTsVector), _tsvector },
                { typeof(NpgsqlLogSequenceNumber), _pglsn}
             };
        }

        protected override IReadOnlyDictionary<Type, TypeMapping> GetClrTypeMappings()
        {
            return _clrTypeMappings;
        }

        protected override IReadOnlyDictionary<string, TypeMapping> GetDbTypeMappings()
        {
            return _dbTypeMappings;
        }

        public override TypeMapping FindMapping(Type clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            clrType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(clrType));

            var typeMapping = clrType == typeof(string)
                ? _internaleString
                : (clrType == typeof(byte[])
                    ? _bytea
                    : base.FindMapping(clrType));

            if (typeMapping == null && clrType == typeof(DBNull))
            {
                return TypeMapping.NullMapping;
            }

            if (typeMapping == null)
            {
                typeMapping = this.FindArrayMapping(clrType);
            }

            return typeMapping;
        }

        public override TypeMapping FindMapping(string dbTypeName)
        {
            TypeMapping mapping = null;

            if (String.IsNullOrWhiteSpace(dbTypeName) || dbTypeName.EndsWith("[]"))
            {
                mapping = this.FindArrayMapping(dbTypeName);
            }

            if (mapping == null)
            {
                mapping = base.FindMapping(dbTypeName);
            }

            return mapping;
        }

        public override TypeMapping FindMapping(Type clrType, string dbTypeName)
        {
            TypeMapping mapping = null;

            if (!String.IsNullOrWhiteSpace(dbTypeName))
            {
                mapping = this.FindMapping(dbTypeName);

                if (mapping != null &&
                    mapping.ClrType != null &&
                    mapping.ClrType == typeof(char) &&
                    clrType == typeof(string))
                {
                    return _stringAsSingleChar;
                }
            }

            return mapping ?? this.FindMapping(clrType);
        }

        private TypeMapping FindArrayMapping(string dbTypeName)
        {
            var elementDbType = dbTypeName.Substring(0, dbTypeName.Length - 2);

            var elementTypeMapping = elementDbType.StartsWith("character", StringComparison.OrdinalIgnoreCase)
                ? _varchar : this.FindMapping(elementDbType);

            if (elementTypeMapping != null)
            {
                return new PostgreSqlArrayListTypeMapping(dbTypeName, elementTypeMapping);
            }

            return null;
        }

        private TypeMapping FindArrayMapping(Type clrType)
        {
            if (clrType == null || !clrType.IsArray)
            {
                return null;
            }

            var elementType = clrType.GetElementType();

            if (_clrTypeMappings.TryGetValue(elementType, out var mapping))
            {
                return new PostgreSqlArrayListTypeMapping(mapping.DbTypeName + "[]", mapping);
            }

            return null;
        }
    }
}
